package com.syspeak.modules.spring.web.method.annotation.support;

import java.lang.annotation.Annotation;

import javax.servlet.ServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.core.MethodParameter;
import org.springframework.validation.BindException;
import org.springframework.validation.Errors;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.bind.support.WebRequestDataBinder;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.annotation.ModelFactory;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import com.syspeak.modules.spring.web.json.JsonConfigedObject;
import com.syspeak.modules.spring.web.servlet.mvc.annotation.JsonConfig;

public class JsonConfigMethdArgumentProcessor implements HandlerMethodArgumentResolver {

	protected final Logger logger = LoggerFactory.getLogger(getClass());

	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		Class<?> type = parameter.getParameterType();
		return JsonConfigedObject.class.isAssignableFrom(type)
				&& parameter.getMethodAnnotation(JsonConfig.class) != null;
	}

	@Override
	public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
		String name = ModelFactory.getNameForParameter(parameter);
		Object target = (mavContainer.containsAttribute(name)) ? mavContainer.getModel().get(name) : createAttribute(
				name, parameter, binderFactory, webRequest);
		Class<?> type = parameter.getParameterType();
		if (JsonConfigedObject.class.isAssignableFrom(type)) {
			JsonConfig jsonConfig = parameter.getMethodAnnotation(JsonConfig.class);
			if (jsonConfig != null) {
				((JsonConfigedObject) target).setJsonConfig(jsonConfig);
			}
		}

		WebDataBinder binder = binderFactory.createBinder(webRequest, target, name);

		if (binder.getTarget() != null) {
			bindRequestParameters(binder, webRequest);

			if (isValidationApplicable(binder.getTarget(), parameter)) {
				binder.validate();
			}

			if (binder.getBindingResult().hasErrors()) {
				if (isBindExceptionRequired(binder, parameter)) {
					throw new BindException(binder.getBindingResult());
				}
			}
		}

		mavContainer.addAllAttributes(binder.getBindingResult().getModel());

		return binder.getTarget();
	}

	/**
	 * Extension point to create the model attribute if not found in the model.
	 * The default implementation uses the default constructor.
	 * @param attributeName the name of the attribute, never {@code null}
	 * @param parameter the method parameter
	 * @param binderFactory for creating WebDataBinder instance
	 * @param request the current request
	 * @return the created model attribute, never {@code null}
	 */
	protected Object createAttribute(String attributeName, MethodParameter parameter,
			WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception {
		return BeanUtils.instantiateClass(parameter.getParameterType());
	}

	/**
	 * Whether to validate the model attribute.
	 * The default implementation checks for {@code @javax.validation.Valid}. 
	 * @param modelAttribute the model attribute
	 * @param parameter the method argument
	 */
	protected boolean isValidationApplicable(Object modelAttribute, MethodParameter parameter) {
		Annotation[] annotations = parameter.getParameterAnnotations();
		for (Annotation annot : annotations) {
			if ("Valid".equals(annot.annotationType().getSimpleName())) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Extension point to bind the request to the target object.
	 * @param binder the data binder instance to use for the binding
	 * @param request the current request
	 * @throws java.net.BindException 
	 */
	protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
		if (binder instanceof WebRequestDataBinder) {
			((WebRequestDataBinder) binder).bind(request);
		} else if (binder instanceof ServletRequestDataBinder) {
			ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
			ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
			servletBinder.bind(servletRequest);
		} else {
			logger.error("Binder " + binder + " is invalid!");
		}
	}

	/**
	 * Whether to raise a {@link BindException} on bind or validation errors.
	 * The default implementation returns {@code true} if the next method 
	 * argument is not of type {@link Errors}.
	 * @param binder the data binder used to perform data binding
	 * @param parameter the method argument
	 */
	protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
		int i = parameter.getParameterIndex();
		Class<?>[] paramTypes = parameter.getMethod().getParameterTypes();
		boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));

		return !hasBindingResult;
	}
}
